本篇大綱:d3.stack( ) 的用法、本次範例的畫面與互動效果、繪製堆積長條圖
今天的一天一圖表,來到長條圖三部曲的終章 — 堆積長條圖!
堆積長條圖的繪製相對困難一點,我們需要使用到 d3.scaleBand( ) 跟 d3.stack( )  這兩個API。
這個方法主要是用來繪製堆積圖表,我們會使用 d3.stack( ) 的方法來換算每個數據的占比,接著再把這些數據呈現在長條圖上。如果不清楚它要怎麼使用、會生成哪些方法、提供什麼樣的數據的話,我在 Day9 的 Layouts 章節有詳細的解說。
這次我們要做的範例畫面與互動效果有:
我們這次沿用昨天的資料(臺南市勞動人口)來製作~首先,一樣先建立 svg 並把資料取回來
// css
.chart{
    width: 100%;
    min-width: 300px;
    margin: auto;
}
// html
<div class="chart"></div>
// js
let data = []
async function getData() {
  // 取資料
  dataGet = await d3.csv('./data/tainan_labor_force_population.csv')
  data = dataGet
  console.log(data)
  drawBarChart()
};
getData()
// RWD
function drawBarChart(){
  // 刪除原本的svg.charts,重新渲染改變寬度的svg
  d3.select('.chart svg').remove();
  // RWD 的svg 寬高
  const rwdSvgWidth = parseInt(d3.select('.chart').style('width')),
        rwdSvgHeight = rwdSvgWidth,
        margin = 20,
        marginBottom = 100
  const svg = d3.select('.chart')
                .append('svg')
                .attr('width', rwdSvgWidth)
                .attr('height', rwdSvgHeight);
  // 接下來的程式碼放這邊...
  // 接下來的程式碼放這邊...
  // 接下來的程式碼放這邊...
}
d3.select(window).on('resize', drawBarChart);
接下來,我們把 X軸跟 Y軸要用到的資料分別整理出來,並依此來建立 X軸跟 Y軸。
// map 資料集
const xData = data.map((i) => i['年度']);
// 設定要給 X 軸用的 scale 跟 axis
const xScale = d3.scaleBand()
                .domain(xData)
                .range([margin*2, rwdSvgWidth - margin]) // 寬度
                .padding(0.6)
const xAxis = d3.axisBottom(xScale)
// 呼叫繪製x軸、調整x軸位置
const xAxisGroup = svg.append("g")
                      .call(xAxis)
                      .attr("transform", `translate(0,${rwdSvgHeight - marginBottom})`)
// 設定要給 Y 軸用的 scale 跟 axis
const yScale = d3.scaleLinear()
                .domain([0, 1200])
                .range([rwdSvgHeight - marginBottom, margin])
                .nice() // 補上終點值
const yAxis = d3.axisLeft(yScale)
                .ticks(5)
                .tickSize(3)
// 呼叫繪製y軸、調整y軸位置
const yAxisGroup = svg.append("g")
                      .call(yAxis)
                      .attr("transform", `translate(${margin*2},0)`)
重點來了!除了本來的X軸分組之外,我們還需要整理一個 subgroups 資料集。這個資料集是把我們想要分組的資料拉出來,之後用來建立堆積圖的。以這邊的資料來說,我們希望能分成四組:
因此,我們用 Object.keys(data[0]).slice(1) 把這四個組別拉出來。分組的資料拉出來後,接著就是用 d3.stack( ) 的方法,把這些資料變成堆積圖可以使用的數據。
// 拉出要分組的資料
const subgroups =  Object.keys(data[0]).slice(1)
// 用 d3.stack() 把資料堆疊起來
const stackedData = d3.stack()
                      .keys(subgroups)(data)
接著,我們用 scaleOrdinal 的方法來設定 subgorup 資料的顏色
// 設定不同 subgorup bar的顏色
const color = d3.scaleOrdinal()
  .domain(subgroups)
  .range(['#e41a1c','#377eb8','#4daf4a', '#ffda6b'])
再來就是建立堆積圖表啦!我們把用 d3.stack 建立好的資料帶進去,並使用它提供的資料去建立 < rect >
const bar = svg.append('g')
                 .selectAll('g')
                 .data(stackedData)
                 .join('g')
                 .attr('fill',  d => color(d.key))
                 .selectAll('rect')
                 .data(d=>d)
                 .join('rect')
                 .attr("x", d => xScale(d.data['年度']))
                 .attr("y", d => yScale(d[1]))
                 .attr("height", d => yScale(d[0]) - yScale(d[1]))
                 .attr("width",xScale.bandwidth())
這樣基本的長條堆疊圖表就完成了

別走,還沒結束!我們還有動畫跟下方的標籤說明要做呢,先來加動畫吧
bar.on("mouseover", handleMouseOver)
   .on("mouseleave", handleMouseLeave)
// 設定文字標籤
const textTag = svg.append('text')
                  .attr('class', 'infoText')
                  .style('fill', '#000')
                  .style('font-size', '18px')
                  .style('font-weight', 'bold')
                  .style("text-anchor", 'middle')
                  .style('opacity', '0')
function handleMouseOver(d, i){
  const pt = d3.pointer(event, svg.node())
  d3.select(this)
    .style('opacity', '0.5')
  // 加上文字標籤
  textTag
     .style('opacity', '1')
     .attr("x",  pt[0])
     .attr('y', pt[1]-20)
     .text((d.target.__data__[1] - d.target.__data__[0]) + '千人')
}
function handleMouseLeave(){
  d3.select(this)
    .style('opacity', '1')
  
  textTag.style('opacity', '0')
}
最後再加上最下方的標籤就可以了~
// 加上辨識標籤
const tagsWrap =  svg.append('g')
     .selectAll('g')
     .attr('class', 'tags')
     .data(subgroups)
     .enter()
     .append('g')
if(window.innerWidth < 780){
  tagsWrap.attr('transform', "translate(-70,0)")
}
    
tagsWrap.append('rect')
     .attr('x', (d,i)=> (i+1)*marginBottom*1.3)
     .attr('y', rwdSvgHeight-marginBottom/2)
     .attr('width', 20)
     .attr('height', 20)
     .attr('fill', d => color(d))
tagsWrap.append('text')
        .attr('x', (d,i)=> (i+1)*marginBottom*1.3)
        .attr('y', rwdSvgHeight-10)
        .style('fill', '#000')
        .style('font-size', '12px')
        .style('font-weight', 'bold')
        .style("text-anchor", 'middle')
        .text(d=>d)
完成!一天又平安的結束惹 (下台一鞠躬)~
最後附上本章的程式碼:想看完整程式碼的請上 Github,想直接操作圖表的則去 Github Page 吧!請自行取用~
請問,如果想做radius(top-left, top-right)
怎麼做呢?
如果只要單邊有弧度,沒辦法單純用處理哦。可以參考這邊的作法 https://medium.com/@dennismphil/one-side-rounded-rectangle-using-svg-fb31cf318d90